package com.example.seckillweb.service.impl;

import com.example.seckillweb.mapper.Goods;
import com.example.seckillweb.mapper.GoodsMapper;
import com.example.seckillweb.service.GoodsService;
import com.example.seckillweb.service.OrderService;
import lombok.extern.slf4j.Slf4j;
import org.redisson.Redisson;
import org.redisson.api.RLock;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;

import java.time.Duration;
import java.util.Collections;
import java.util.concurrent.TimeUnit;

/**
* @author smc
* @description 针对表【goods】的数据库操作Service实现
* @createDate 2023-12-18 21:25:55
*/
@Slf4j
@Service
public class GoodsServiceImpl implements GoodsService {

    @Autowired
    private GoodsMapper goodsMapper;
    @Autowired
    private OrderService orderService;
    @Autowired
    private StringRedisTemplate redisTemplate;
    @Autowired
    private Redisson redisson;

    public static final String REDIS_LOCK = "lock:xxx";


    public Goods findGoodsByGoodsId(Integer goodsId){
        return goodsMapper.selectByGoodsId(goodsId);
    }

    @Override
    public int deductStock(Goods goods) {
        return goodsMapper.updateByPrimaryKeySelective(goods);
    }

    @Override
    public int deductStock(Integer goodsId) {
        return goodsMapper.deductStock(goodsId);
    }

    @Transactional(rollbackFor = Exception.class)
    @Override
    public void secKill1(Integer goodsId, Integer userId) {
        // 1. 查库存
        Goods goods = findGoodsByGoodsId(goodsId);
        if (goods.getTotalStocks() <= 0) {
            log.info("库存不足");
            return;
        }
        goods.setTotalStocks(goods.getTotalStocks() - 1);
        // 2. 减库存
        int count = deductStock(goods);
        if (count <= 0) {
            log.info("stock deduck fail");
            return;
        }
        // 3. 创建订单
        count = orderService.generateOrder(goodsId, userId);
        if (count <= 0){
            log.info("订单生成失败");
            return;
        }
    }

    // 方法一： 使用mysql行锁解决， 如果并发太大，mysql压力过大
    @Transactional
    @Override
    public void secKill2(Integer goodsId, Integer userId) {
        // 1. 扣减库存
        int count = goodsMapper.deductStock(goodsId);
        if (count <= 0) {
            log.info("userId={}, goodId={},空间库存失败",userId, goodsId);
            return;
        }
        // 2. 创建订单
        count = orderService.generateOrder(goodsId, userId);
        if (count <= 0){
            log.info("订单生成失败");
            return;
        }

    }

    // 方法二：使用synchronized 锁， 使线程串行化，效率较低，并且使单机的，锁是this。
    @Override
    public void secKill3(Integer goodsId, Integer userId) {
        synchronized (this){
            secKill1(goodsId, userId);
        }
    }

    // 方法三：使用redis setnx 分布式锁，这种方式有问题：锁时间到期了，事务还没结束，上锁和删锁可以不同人。
    @Override
    public void secKill4(Integer goodsId, Integer userId) {
        Boolean res = redisTemplate.opsForValue().setIfAbsent("lock:" + goodsId, "exit", Duration.ofMillis(10000));
        while(true){
            if (res){
                try {
                    secKill2(goodsId, userId);
                    return;
                } finally {
                    redisTemplate.delete("lock:" + goodsId);
                }
            }else {
                try {
                    Thread.sleep(200);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        }
    }

    // 对redis setnx 的分布式锁进行改进1： 存在的问题：
    // 1. 多线程的并发下，由于改key是所有线程共享同一个，会导致上锁和解锁的线程不是同一个
    // 2. 锁时间到期了，事务还没结束，锁过期释放了，当该事务结束的时候，删除另一个线程的锁。
    public void secKill5(Integer goodsId, Integer userId) {
        String clientId = String.valueOf(Thread.currentThread().getId());
        try {
            Boolean res = redisTemplate.opsForValue().setIfAbsent(REDIS_LOCK, clientId, Duration.ofMillis(10000));
            if (!res){
                log.info("没有抢到锁");
                return;
            }
            secKill1(goodsId, userId);
            return;
        } finally {
            redisTemplate.delete(REDIS_LOCK);
        }
    }
    // 对redis setnx 的分布式锁进行改进2：在删除锁的时候，对上锁者和删除锁者进行判断，相同才能删除。存在的问题：
    // 1. 锁时间到期了，事务还没结束，锁释放了，当该事务结束的时候，删除另一个线程的锁。
    // 2. 在删除锁的由于不具有原子性，在判断条件成功后发生了故障直到锁过期了另一个线程获取到锁了，然后故障恢复，删除掉了另一个线程获取的锁
    public void secKill6(Integer goodsId, Integer userId) {
        String clientId = String.valueOf(Thread.currentThread().getId());
        try {
            Boolean res = redisTemplate.opsForValue().setIfAbsent(REDIS_LOCK, clientId, Duration.ofMillis(10000));
            if (!res){
                log.info("没有抢到锁");
                return;
            }
            secKill1(goodsId, userId);
            return;
        } finally {
            // 下面两个操作不具有原子性
            if (clientId.equals(redisTemplate.opsForValue().get(REDIS_LOCK))) {
                // 在释放锁的时候判断是不是自己上的锁
                redisTemplate.delete(REDIS_LOCK);
            }
        }
    }

    // 对redis setnx 的分布式锁进行改进3：对删除锁的操作进行原子化（添加redisson使用lua脚本）
    // 存在问题：业务没有结束，锁过期
    public void secKill7(Integer goodsId, Integer userId) {
        String clientId = String.valueOf(Thread.currentThread().getId());
        while (true){
            Boolean res = redisTemplate.opsForValue().setIfAbsent(REDIS_LOCK, clientId, Duration.ofMillis(10000));
            if (res){
                try {
                    secKill1(goodsId, userId);
                    return;
                } finally {
                    JedisPool jedisPool = new JedisPool("myhost", 6379);
                    Jedis jedis = null;
                    try {
                        jedis = jedisPool.getResource();
                        String script = """
                        if redis.call('get', KEYS[1]) == ARGV[1] then
                            return redis.call('del', KEYS[1])
                        end
                        return 0
                        """;
                        Object eval = jedis.eval(script, Collections.singletonList(REDIS_LOCK), Collections.singletonList(clientId));
                        if ("1".equals(eval)) {
                            log.info("锁释放成功");
                        }else {
                            log.info("锁释放发生异常");
                        }
                    } catch (Exception e) {
                        throw new RuntimeException(e);
                    }
                }
            }else {
                log.info("没有抢到锁");
                try {
                    Thread.sleep(30);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        }
    }

    // 使用redisson 实现锁续命， 该方式解决上以上的问题，但是如果跑在高可用集群上，会出现锁丢失问题
    @Override
    public void secKill8(Integer goodsId, Integer userId) {
        RLock rLock = redisson.getLock(REDIS_LOCK);
        try {
            // 这种方式获取锁失败后直接放回
//            boolean lock = rLock.tryLock();
            // 设置锁的有效期为5s，最大等待时间为20s
            boolean lock = rLock.tryLock(20, 5, TimeUnit.SECONDS);
            if (lock){
                secKill1(goodsId, userId);
            }
        } catch (Exception e) {
            throw new RuntimeException(e);
        }finally {
            rLock.unlock();
        }
    }

}




